# File: autotester/html.py
# Author: Carl Allendorph
# Date: 20NOV2016
#
# Description:
#   This file contains the HTML generation code for the autotester.
# This includes generating the initial HTML file as output of the
# python run, and the javascript that builds the results at runtime
# in the browser.
#

from org.transcrypt.stubs.browser import __main__, __envir__, __pragma__

__pragma__ ('nokwargs')

okColor = 'green'
errorColor = 'red'
highlightColor = 'yellow'
testletNameColor = 'blue'

messageDivId = 'message'
referenceDivId = 'python'
refResultDivId = "pyresults"
refPosDivId = "pypos"
testDivId = 'transcrypt'
tableId = 'resulttable'
resultsDivId = 'results'
faultRowClass = 'faultrow'
testletHeaderClass = "testletheader"
transValClass = "trans-val"
transPosClass = "trans-pos"
pyValClass = "py-val"
pyPosClass = "py-pos"
excAreaId = "exc-area"
excHeaderClass = "exc-header"
forceCollapseId = "force-collapse"
forceExpandId = "force-expand"

def itemsAreEqual (item0, item1):
    # Don't be picky on whitespace, since that would hinder comparing docstrings
    return ' '.join (item0.split ()) == ' '.join (item1.split ())

class HTMLGenerator(object):
    """ This class generates the HTML template for the autotester results.
    This code is primarily run during the Python execution cycle but
    does define strings that are referenced by the javascript as well.
    """
    def __init__(self, filenameBase = None):
        """
        @param filenameBase string denoting the base filename for the
        files this class will generate. This should NOT include the
        file extension (.html for example). This will be tacked on
        later depending on minified state.
        """
        self._fnameBase = filenameBase

    def generate_html(self, refDict):
        """ Generate the HTML file that gets generated by the
        Python script portion of the test. File will be named
        '<fnameBase.html'
        @param refDict Dict of python reference data associated with
          each of the test modules. The keys of this dict are the names
          of the ref modules.
        """
        
        minified = False # Remove param 'minified' altogether and 
        
        if ( self._fnameBase is None ):
            raise ValueError("Filename Base must be defined to generate")
        fname = self._fnameBase + '.html'

        print (f'Generating {fname}')
        
        jsPath = f'__target__/{self._fnameBase.split ("/")[-1]}.js'

        with open( fname, 'w', encoding = 'UTF-8') as f:
            f.write("<html><head><meta charset = 'UTF-8'>")
            self._writeCSS(f)
            f.write("</head><body>")

            self._writeStatusHeaderTemplate(f)

            dc = DataConverter()
            dc.writeHiddenResults(f, refDict)

            self._writeTableArea(f)

            f.write ('<script type="module" src="{}"></script>\n\n'.format (jsPath))
            f.write("</body></html>")

    ##########################
    # Internal Methods
    ##########################

    def _writeCSS(self, f):
        cssOut = """
        <style>
          body {
            max-width: 100%;
          }
          .faultrow > td {
             background-color: LightCoral;
          }
          #resulttable {
            border-collapse: collapse;
            width: 100%;
            table-layout: fixed;
          }
          #resulttable th, #resulttable td {
            border: 1px solid grey;
          }
          .testletheader > td {
            background-color: LightSkyBlue;
          }
          .header-pos {
            width: 20%;
          }
          .header-val {
            width: 30%;
          }
          .py-pos,.trans-pos {
            width: 20%;
            overflow: hidden;
          }
          .py-val, .trans-val {
            width: 30%;
            overflow-x: auto;
          }
          .exc-header {
          color: red;
          }
          .collapsed {
            display: None;
          }
        </style>
        """
        f.write(cssOut)

    def _writeStatusHeaderTemplate(self, f):
        f.write ('<b>Status:</b>\n')
        f.write ('<div id="{}"></div><br><br>\n\n'.format (messageDivId))

    def _writeTableArea(self, f):
        f.write ('<div id="{}"></div>'.format(excAreaId))
        f.write ('<div id="{}">'.format(resultsDivId))
        f.write ('<div> <a id="{}" href="#"> Collapse All</a> <a id="{}" href="#">Expand All</a></div>'.format(forceCollapseId, forceExpandId))
        f.write ('<table id="{}"><thead><tr> <th colspan="2"> CPython </th> <th colspan="2"> Transcrypt </th> </tr>'.format(tableId))
        f.write ('<tr> <th class="header-pos"> Location </th> <th class="header-val"> Value </th> <th class="header-val"> Value </th> <th class="header-pos"> Location </th> </tr></thead><tbody></tbody>')
        f.write ('</table>')
        f.write ('</div>')

class DataConverter(object):
    """ This class contains code that stores the python results in
    the HTML document and can extract the data from the HTML to
    prepare it for comparison with the javascript results.
    """

    def writeHiddenResults(self, f, refDict):
        """ Write the Python results into a div that is hidden by
        default so that we can extract it at runtime.
        @param f file that we are writing the content into
        @param refDict python reference result data in the form of
        a dict. The keys are the names of the individual test modules.
        """
        f.write('<div id="{}" style="display: None">'.format(referenceDivId))
        for key in refDict.keys():
            itemData = ' | '.join([x[1] for x in refDict[key]])
            posContent = ' | '.join([x[0] for x in refDict[key]])
            f.write('<div id="{}">\n'.format(key))
            # @note - we should probably HTML escape this
            #    data so that we don't get the HTML rendering
            #    engine mucking with our test result.
            f.write ('<div id="{}">{}</div>\n\n'.format (refResultDivId, itemData))
            f.write ('<div id="{}">{}</div>\n'.format(refPosDivId, posContent))
            f.write('</div>\n')
        f.write('</div></div>\n')

    def getPythonResults(self):
        """ Acquire the python unit test results from the
            hidden div and parse into a dictionary.
        @return dict whose keys are the names of the test
          submodules.
        """
        refData = document.getElementById(referenceDivId)
        # Each of the children of this element is in the form
        # <div id="{key}">
        #   <div id="pyresults"> {Result Content} </div>
        #   <div id="pypos"> {Result Positions} </div>
        # </div>
        refDict = {}
        for child in refData.children:
            keyName = child.getAttribute("id")
            posData,resultData = self._extractPosResult(child)
            refDict[keyName] = zip(posData, resultData)
        return(refDict)

    def _extractPosResult(self, elem):
        resultData = None
        posData = None
        for e in elem.children:
            idStr = e.getAttribute("id")
            if ( idStr == refResultDivId):
                resultData = e.innerHTML.split(' | ')
            elif ( idStr == refPosDivId):
                posData = e.innerHTML.split(' | ')
            else:
                # Unknown Element - very strange
                pass
        return(posData, resultData)


def getRowClsName(name):
    """ Utility method for naming the test module class that
        a row belows to
    """
    return("mod-" + name)


class JSTesterUI(object):
    """ This class contains the code that populates the autotester results
    while running in the javascript runtime.
    """
    def __init__(self):
        """
        """
        self.expander = TestModuleExpander()

    def setOutputStatus(self, success):
        if ( success ):
            document.getElementById(messageDivId).innerHTML = '<div style="color: {}">Test succeeded</div>'.format (okColor)
        else:
            document.getElementById(messageDivId).innerHTML = '<div style="color: {}"><b>Test failed</b></div>'.format (errorColor)

    def appendSeqRowName(self, name, errCount):
        """
        """
        table = document.getElementById(tableId)
        # Insert at the end
        row = table.insertRow(-1);
        row.id = name
        row.classList.add(testletHeaderClass)
        self.expander.setupCollapseableHeader(row, (errCount == 0))

        # Populate the Row
        headerCell = row.insertCell(0)
        headerCell.innerHTML = name + " | Errors = " + str(errCount)
        headerCell.colSpan = 4
        headerCell.style.textAlign= "center"


    def appendTableResult(self, name, testPos, testItem, refPos, refItem, collapse=False):
        clsName = getRowClsName(name)

        table = document.getElementById(tableId)
        # Insert at the end
        row = table.insertRow(-1);
        row.classList.add(clsName)
        if not itemsAreEqual (testItem, refItem) :
            row.classList.add(faultRowClass)
            refPos = "!!!" + refPos
        else:
            self.expander.setCollapsed(row, collapse)

        # Populate the Row
        cpy_pos = row.insertCell(0)
        cpy_pos.innerHTML = refPos
        cpy_pos.classList.add(pyPosClass)
        cpy_val = row.insertCell(1)
        cpy_val.innerHTML = refItem
        cpy_val.classList.add(pyValClass)
        trans_val = row.insertCell(2)
        if ( testItem is not None ):
            trans_val.innerHTML = testItem
        trans_val.classList.add(transValClass)
        trans_pos = row.insertCell(3)
        if ( testPos is not None ):
            trans_pos.innerHTML = testPos
        trans_pos.classList.add(transPosClass)


    def showException(self, testname, exc):
        """
        """
        excElem = document.getElementById(excAreaId)
        header = document.createElement("H2")
        header.classList.add(excHeaderClass)
        header.innerHTML = "Exception Thrown in JS Runtime";
        excElem.appendChild(header)
        content = document.createElement("p")
        content.innerHTML = "Exception in {}: {}".format(testname, str(exc))
        excElem.appendChild(content)
        stacktrace = document.createElement("p")
        if ( exc.stack is not None ):
            stacktrace.innerHTML = str(exc.stack)
        else:
            stacktrace.innerHTML = "No Stack Trace Available!"


class TestModuleExpander(object):
    """ This class handles expanding or contracting a set of
    test row results under a particular test.
    """
    def __init__(self):
        """
        """
        self.collapsedClass = "collapsed"
        self.modCollapseClass = "mod-collapsed"

        self._expandCollapseAllFuncs()

    def setCollapsed(self, row, collapse):
        """
        """
        if ( collapse ):
            row.classList.add(self.collapsedClass)
        else:
            row.classList.remove(self.collapsedClass)


    def setupCollapseableHeader(self, row, startCollapsed = False):
        """
        """
        if ( startCollapsed ):
            row.classList.add(self.modCollapseClass)

        def toggleCollapse(evt):
            """ Toggle whether the
            """
            headerRow = evt.target.parentElement
            doCollapse = not headerRow.classList.contains(self.modCollapseClass)
            self.collapseModule(headerRow, doCollapse)

        row.onclick = toggleCollapse


    def collapseModule(self, headerRow, doCollapse):
        """ collapse/expand particular module in the table of results
        """
        name = headerRow.id
        table = document.getElementById(tableId)
        clsName = getRowClsName(name)
        allRows = table.tHead.children
        rows = filter(lambda x: x.classList.contains(clsName), allRows)

        for row in rows:
            self.setCollapsed(row, doCollapse)

        if ( doCollapse ):
            headerRow.classList.add(self.modCollapseClass)
        else:
            headerRow.classList.remove(self.modCollapseClass)

    def _expandCollapseAllFuncs(self):
        """ This function sets up the callback handlers for the
        collapse all and expand all links
        """

        def applyToAll(evt, collapse):
            """
            """
            table = document.getElementById(tableId)

            # find all rows in the testletheader class
            filtFunc = lambda x: x.classList.contains(testletHeaderClass)
            headerRows = filter(filtFunc, table.tHead.children)

            for headerRow in headerRows:
                self.collapseModule(headerRow, collapse)

        def collapseAll(evt):
            """ collapse all rows handler
            """
            evt.preventDefault()
            applyToAll(evt, True)
            return(False)

        def expandAll(evt):
            """ Expand All Rows Handler
            """
            evt.preventDefault()
            applyToAll(evt, False)
            return(False)

        forceCollapse = document.getElementById(forceCollapseId)
        forceCollapse.onclick = collapseAll

        forceExpand = document.getElementById(forceExpandId)
        forceExpand.onclick = expandAll
